package roborally;

import items.*;
import elements.*;

import java.util.HashSet;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;

import condition.*;

import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Immutable;

import roborally.Orientation;
import exceptions.IllegalPlacementException;
import gui.Facade;

/**
 * @invar	| canHaveAsHeight(this.getHeight())
 * @invar	| canHaveAsWidth(this.getWidth())
 * 
 * @version	3.0
 * @author 	Lies Tambeur 	r0264469	2Bcwsbbh
 * @author	Eline Vanrykel	r0264543	2Bcwselt 
 */
public class Board extends Terminatable{
	/**
	 * The constant reflecting the highest possible x-value for the size of the playing board.
	 * @invar	The variable has to be a positive value and smaller than or equal to <code> Long.MAX_VALUE </code>.
	 * 			|  MAX_X_COORD > 0 && MAX_X_COORD <= Long.MAX_VALUE
	 */
	private static final long MAX_X_COORD = Long.MAX_VALUE;
	
	/**
	 * The constant reflecting the highest possible y-value for the size of the playing board.
	 * @invar	The variable has to be a positive value and smaller than or equal to <code> Long.MAX_VALUE </code>.
	 * 			|  MAX_Y_COORD > 0 && MAX_Y_COORD <= Long.MAX_VALUE
	 */
	private static final long MAX_Y_COORD = Long.MAX_VALUE;
	
	/**
	 * Returns the constant reflecting the highest possible x-value for the size of the playing board.
	 * @return 	The constant reflecting the highest possible value for the size of the playing board.
	 * 			| result = MAX_COORD
	 */
	@Basic @Immutable
	public static long getMaxWidth(){
		return MAX_X_COORD;
	}
	
	/**
	 * Returns the constant reflecting the highest possible y-value for the size of the playing board.
	 * @return 	The constant reflecting the highest possible value for the size of the playing board.
	 * 			| result = MAX_COORD
	 */
	@Basic @Immutable
	public static long getMaxHeight(){
		return MAX_Y_COORD;
	}
	
	private Map<Position,Set<Element>> elements;
	private final long width;
	private final long height;
	
	/**
	 * @param 	width
	 * @param 	height
	 * @post	| (Board.canHaveAsHeight(Math.abs(height)) == (new.getHeight() == Math.abs(height))
	 * @post	| (!Board.canHaveAsHeight(Math.abs(height)) == (new.getHeight() == Board.getMaxHeight())
	 * @post	| (Board.canHaveAsWidth(Math.abs(width))) == (new.getWidth() == Math.abs(width))
	 * @post	| (!Board.canHaveAsWidth(Math.abs(width))) == (new.getWidth() == Board.getMaxWidth())
	 */
	public Board(long width, long height){ 
		elements = new HashMap<Position,Set<Element>>();
		if(canHaveAsHeight(Math.abs(height))){
			this.height = Math.abs(height);
		}
		else{
			this.height = Board.getMaxHeight();
		}
		if(canHaveAsWidth(Math.abs(width))){
			this.width = Math.abs(width);
		}
		else{
			this.width = Board.getMaxWidth();
		}
	}

	/**
	 * @return	| result == new HashMap<Position,Set<Element>>(this.elements)
	 */
	@Basic
	public Map<Position,Set<Element>> getElements(){
		return new HashMap<Position,Set<Element>>(this.elements);
	}
	
	/**
	 * @pre |T == clazz.class
	 * @param clazz
	 * @return	| for each element in this.getAllElements():
	 * 			|	if element instanceof T
	 * 			|		then result.contains(element)
	 */
	@SuppressWarnings("unchecked")
	public <T extends Element> Set<T> getElementsOfType(Class<T> clazz){
		Set<T> elements = new HashSet<T>();
		Iterator<Element> it = this.getAllElements().iterator();
		while(it.hasNext()){
			Element element = it.next();
			if(clazz.isAssignableFrom(element.getClass())){
				elements.add((T)element);
			}
		}
		return elements;
	}

	
	/**
	 * @param 	height
	 * @return	| result == (height>0) && (height <= getMaxHeight())
	 */
	public static boolean canHaveAsHeight(long height){ 
		return (height>0) && (height <= getMaxHeight());
	}
	
	/**
	 * @param 	width
	 * @return	| result == (width>0) && (width <= getMaxWidth())
	 */
	public static boolean canHaveAsWidth(long width){ 
		return (width>0) && (width <= getMaxWidth());
	}
	
	/**
	 * @param 	position
	 * @return	| if(this.getElements().containsKey(position))
	 * 			|	then result == this.getElements().get(position)
	 * 			| else
	 * 			| 	result == new HashSet<Element>()
	 */
	public Set<Element> getElements(Position position){ 
		if(this.getElements().get(position)!=null){
			return new HashSet<Element>(this.getElements().get(position));
		}
		else{
			return new HashSet<Element>();
		}
	}
	
	/**
	 * @param position
	 * @return	| for each element in getElements(position):
	 * 			|		if( element instanceof Item)
	 * 			|			then result.contains(element)
	 */
	public Set<Item> getItems(Position position){
		Set<Element> elements = this.getElements(position);
		Set<Item> items = new HashSet<Item>();
		Iterator<Element> it = elements.iterator();
		while(it.hasNext()){
			Element element = it.next();
			if(element instanceof Item){
				items.add((Item) element);
			}
		}
		return items;
	}
	
	/**
	 * @param	orientation
	 * @return 	| if (result != null)
	 * 			|	then 	maxI = Math.abs(result.getPosition().getY()-thePosition.getY())+result.getPosition().getX()-thePosition.getX())-1
	 * 			|			for each i=1..maxI:
	 * 			|				this.getElement(i times thePosition.getNeighbour(orientation)) == null
	 * 			|			this.getElement(maxI+1 times thePosition.getNeighbour(orientation)).contains(result)
	 * 			| if (result == null )
	 * 			|	then 	if( orientation == Orientation.UP || orientation == Orientation.DOWN)
	 * 			|				then:
	 * 			|					if (orientation == Orientation.UP) then max = 0
	 * 			|					if (orientation == Orientation.DOWN) then max = this.getMaxHeight()
	 * 			|					maxI = Math.abs(max-thePosition.getY()))-1
	 * 			|					for each i=1..maxI:
	 * 			|						this.getElement(i times thePosition.getNeighbour(orientation)) == null
	 * 			|					isPositionOnBoard(maxI+1 times thePosition.getNeighbour(orientation))
				|			if( orientation == Orientation.LEFT || orientation == Orientation.RIGHT)
	 * 			|				then:
	 * 			|					if (orientation == Orientation.LEFT) then max = 0
	 * 			|					if (orientation == Orientation.RIGHT) then max = this.getMaxWidth()
	 * 			|					maxI = Math.abs(max-thePosition.getX()))-1
	 * 			|					for each i=1..maxI:
	 * 			|						this.getElement(i times thePosition.getNeighbour(orientation)) == null
	 * 			|					isPositionOnBoard(maxI+1 times thePosition.getNeighbour(orientation))	 
	 * 
	 */
	public Element getFirstElementStartingAt(Position thePosition, Orientation orientation) {
		Position position = new Position(thePosition.getX(),thePosition.getY()).getNeighbour(orientation);
		while (!this.isPositionInUse(position) && isPositionOnBoard(position)){
			position = position.getNeighbour(orientation);
		}
		if(!isPositionOnBoard(position)){
			return null;
		}
		else{
			return getElements(position).iterator().next();
		}
		
	}
	
	/**
	 * @return	| result == this.height
	 */
	@Basic @Immutable
	public long getHeight() {
		return this.height;
	}
	
	/**
	 * @return	| result == this.width
	 */
	@Basic @Immutable
	public long getWidth() {
		return this.width;
	}
	
	/**
	 * @param	postion
	 * @param	element
	 * @return 	| if (!this.isPositionOnBoard(position))
	 * 			| 	then result == false
	 * 			| else if (!isPositionInUse(position))
	 * 			|	then result == true
	 * 			| else 
	 * 			|	for each elementIt in this.getElements(position):
	 * 			|		if(!element.canSharePositionWith(elementIt)
	 * 			|			then result == false
	 * 			|		else
	 * 			|			result == true
	 */
	public boolean isValidPosition(Position position, Element element) { 
		if(isPositionOnBoard(position)){
			if(isPositionInUse(position)){
				Iterator<Element> it = getElements(position).iterator();
				while(it.hasNext()){
					Element elementit = it.next();
					if(!element.canSharePositionWith(elementit)){
						return false;
					}
				}
				return true;
			} else{
				return true;
			}
		}
		else{
			return false;
		}
	}
	
	/**
	 * @param 	other
	 * @throws 	NullPointerException
	 * 			| other == null
	 * @effect	| for each element in other.getAllElements():
	 * 			|		if(this.isValidPositin(element.getPosition(), element)
	 * 			|			then	other.removeElement(element)
	 * 			|					this.putElement(element.getPosition,element)
	 * 			| other.terminate()
	 */
	public void merge(Board other) throws NullPointerException{ 
		if(other==null){
			throw new NullPointerException();
		}
		HashMap<Position,Set<Element>> elements = new HashMap<Position, Set<Element>>(other.getElements());
		Iterator<Position> it = elements.keySet().iterator();
		while(it.hasNext()){
			Position positie = it.next();
				Iterator<Element> itel = elements.get(positie).iterator();
				while(itel.hasNext()){
					Element element = itel.next();
					if(isValidPosition(positie, element)){
						other.removeElement(element);
						this.putElement(positie, element);
					}
				}
		}
		other.terminate();
	}
	
	/**
	 * @param 	position
	 * @param 	element
	 * @throws 	IllegalPlacementException
	 * 			| !this.isValidPosition(position,element)
	 * @throws	IllegalStateException
	 * 			| element.getBoard() != null
	 * @post	| (new.getElements(position) == this.getElements(position).add(element)
	 */
	public void putElement(Position position, Element element) throws IllegalPlacementException, IllegalStateException{
		if(element.getBoard()!=null){
			throw new IllegalStateException("Robot already placed on a board.");
		}
		else if(this.isValidPosition(position,element)){
			Set<Element> elementsOnPosition = new HashSet<Element>();
			if(isPositionInUse(position)){ 
				elementsOnPosition=this.getElements(position);
				elementsOnPosition.add(element);
				this.elements.put(position, elementsOnPosition);
			} else{
				elementsOnPosition.add(element);
				this.elements.put(position, elementsOnPosition); 
			}
		}
		else{
			throw new IllegalPlacementException("Invalid position");
		}
	}
	
	/**
	 * 
	 * @param 	element
	 * @throws 	IllegalStateException
	 * 			| !this.getElements().contains(element)
	 * @post	| !new.getAllElements().contains(element)
	 */
	public void removeElement(Element element) throws IllegalStateException {
		if(this.getAllElements().contains(element)){ 
			Set<Position> posities = getElements().keySet();
			java.util.Iterator<Position> it = posities.iterator();
			while(it.hasNext()){
				Position positie = it.next();
				Set<Element> elementsInPosition = getElements(positie);
				java.util.Iterator<Element> itn = (new HashSet<Element>(elementsInPosition)).iterator();
				boolean isDone = false;
				while(itn.hasNext() && !isDone){
					Element e = itn.next();
					if(e==element){
						if(elementsInPosition.size()==1){
							elements.remove(positie); //elements en niet getElements()!!!!
						}
						else{
							elementsInPosition.remove(e);
							elements.put(positie, elementsInPosition); //elements en niet getElements()!!!!
						}
						isDone = true;
					}
				}
			}
		}
		else {
			throw new IllegalStateException();
		}
	}
	
	/**
	 * @return	| for each element of result:
	 * 			|	this.getBatteries().contains(element) || this.getWalls().contains(element) || this.getRobots().contains(element)
	 */
	public Set<Element> getAllElements(){ 
		Set<Element> allElements = new HashSet<Element>();
		Iterator <Set<Element>> it = this.getElements().values().iterator();
		while(it.hasNext()){
			Iterator<Element> itElements = it.next().iterator();
			while(itElements.hasNext()){
				Element element = itElements.next();
				allElements.add(element);
			}
		}
		return allElements;
	}
	
	/**
	 * @post	| for each element of this.getAllElements():
	 * 			|	element.terminate()
	 */
	@Override
	public void terminate() {
		Set<Element> elements = new HashSet<>(this.getAllElements());
		Iterator<Element> it = elements.iterator();
		while(it.hasNext()){
			it.next().terminate();
		}
		this.setTerminate();
		Facade.removeBoard(this);
	}
	
	/**
	 * @param 	position
	 * @return	| result == (position.getX()<=this.getWidth() && position.getY()<=this.getHeight() 
				|		&& position.getX()>=0 && position.getY() >=0) && (position != null)
	 */
	public boolean isPositionOnBoard(Position position){
		return (position.getX()<=this.getWidth() && position.getY()<=this.getHeight() 
				&& position.getX()>=0 && position.getY() >=0) && (position != null);
	}
	
	/**
	 * @param 	position
	 * @return	| result == (getElements(position).size() != 0)
	 */
	private boolean isPositionInUse(Position position){
		return (getElements(position).size() != 0);
	}
	
	/**
	 * @return	| result == this.getElements().keySet()
	 */
	public Set<Position> getAllPositionsInUse(){
		return this.getElements().keySet();
	}
	
	/**
	 * @param cond
	 * @return	| List<Elements> elements
	 * 			| for each element in getAllElements():
	 * 			|	if cond.evaluate(element)
	 * 			|		then	elements.contains(element)
	 * 			| result = elements.Iterator()
	 */
	public Iterator<Element> getElements(final Condition cond){
		return new Iterator<Element>(){
			
			private int nbOfElementsReturned = 0;
			private List<Element> elements = null;
			Condition condition = cond;
			
			@Override
			public boolean hasNext() {
				if(elements == null){
					elements = new ArrayList<Element>();
					if(condition == null){
						throw new IllegalArgumentException("Invalid condition");
					}
					Iterator<Element> it = getAllElements().iterator(); 
					while(it.hasNext()){
						Element element = it.next();
						if(condition.evaluate(element)){
							elements.add(element);
						}
					}
				}
				if(elements.size()==0){
					return false;
				}
				return nbOfElementsReturned<elements.size();
			}
						
			@Override
			public Element next() {
				if(!this.hasNext()){
					throw new NoSuchElementException();
				}
				else{
					Element result = elements.get(nbOfElementsReturned);
					nbOfElementsReturned++;
					return result;
				}
			}

			@Override
			public void remove() throws UnsupportedOperationException {
				throw new UnsupportedOperationException();
			}
			
		};
	}
}
